package com.github.beihaifeiwu.myspring.http.feign;

import com.github.beihaifeiwu.myspring.core.SpringContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * Created by liupin on 2017/9/5.
 */
@Slf4j
public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

    private ResourceLoader resourceLoader;

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        registerFeignClients(metadata, registry);
    }

    private void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {

        ClassPathScanningCandidateComponentProvider scanner = getScanner(registry);
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));

        getBasePackages(metadata)
                .stream()
                .map(scanner::findCandidateComponents)
                .filter(Objects::nonNull)
                .flatMap(Collection::stream)
                .filter(candidateComponent -> candidateComponent instanceof AnnotatedBeanDefinition)
                .map(candidateComponent -> (AnnotatedBeanDefinition) candidateComponent)
                .map(AnnotatedBeanDefinition::getMetadata)
                .peek(am -> Assert.isTrue(am.isInterface(), "@FeignClient can only be specified on an interface"))
                .forEach(am -> registerFeignClient(registry, am));
    }

    private void registerFeignClient(BeanDefinitionRegistry registry,
                                     AnnotationMetadata annotationMetadata) {
        String className = annotationMetadata.getClassName();

        if (registry.isBeanNameInUse(className)) {
            log.warn("Bean name {} is already in use", className);
            return;
        }

        BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientFactoryBean.class);
        Map<String, Object> attributes = annotationMetadata
                .getAnnotationAttributes(FeignClient.class.getCanonicalName());
        Assert.notNull(attributes, "@FeignClient not found on interface " + className);

        String alias = annotationMetadata.getClass().getSimpleName() + "FeignClient";
        String qualifier = (String) attributes.get("qualifier");
        if (StringUtils.hasText(qualifier)) {
            alias = qualifier;
        }

        definitionBuilder.addPropertyValue("url", getUrl(attributes));
        definitionBuilder.addPropertyValue("type", className);
        definitionBuilder.addPropertyValue("configurer", attributes.get("configurer"));
        definitionBuilder.addPropertyValue("decode404", attributes.get("decode404"));
        definitionBuilder.addPropertyReference("builder", FeignBuilderFactoryBean.BEAN_NAME);
        definitionBuilder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        definitionBuilder.setLazyInit(true);

        AbstractBeanDefinition beanDefinition = definitionBuilder.getBeanDefinition();
        beanDefinition.setPrimary((Boolean) attributes.get("primary"));
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{ alias });
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

    private Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) {
        Set<String> basePackages = new HashSet<>();

        Map<String, Object> attributes = importingClassMetadata
                .getAnnotationAttributes(EnableFeignClients.class.getCanonicalName());
        if (attributes != null) {
            for (String pkg : (String[]) attributes.get("value")) {
                if (StringUtils.hasText(pkg)) {
                    basePackages.add(pkg);
                }
            }
            for (String pkg : (String[]) attributes.get("basePackages")) {
                if (StringUtils.hasText(pkg)) {
                    basePackages.add(pkg);
                }
            }
            for (Class<?> clazz : (Class[]) attributes.get("basePackageClasses")) {
                basePackages.add(ClassUtils.getPackageName(clazz));
            }
        } else {
            basePackages.addAll(SpringContext.basePackages());
        }

        if (basePackages.isEmpty()) {
            basePackages.add(
                    ClassUtils.getPackageName(importingClassMetadata.getClassName()));
        }
        return basePackages;
    }

    private String getUrl(Map<String, Object> attributes) {
        String url = SpringContext.resolvePlaceHolder((String) attributes.get("value"));
        if (StringUtils.hasText(url)) {
            if (!url.contains("://")) {
                url = "http://" + url;
            }
            try {
                new URL(url);
            } catch (MalformedURLException e) {
                throw new IllegalArgumentException(url + " is malformed", e);
            }
        }
        return url;
    }

    private ClassPathScanningCandidateComponentProvider getScanner(BeanDefinitionRegistry registry) {
        return new ClassPathBeanDefinitionScanner(registry, false) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                return beanDefinition.getMetadata().isInterface()
                        && beanDefinition.getMetadata().isIndependent();
            }
        };
    }

}